﻿// Copyright (C) Norsolutions GmbH
// All rights reserved.
// For conditions of distribution and use, see license.
// Created by: Reng van Oord
// Created:    2010.04.26

using System;
using System.Collections.Generic;
using System.Text;
using System.IO;
using System.Security.Cryptography;
using System.Security;
using System.Runtime.InteropServices;
using System.Security.Cryptography.X509Certificates;
using System.ComponentModel;
using log4net;

namespace Sherwood.Security
{
    /// <summary>
    /// Library with essential asymetric cryptographic operations using the OpenSSL implementation of the RSA algorithm.
    /// </summary>
    public class Rsa
    {
        private static readonly ILog Log = LogManager.GetLogger(typeof(Rsa));

        /// <summary>
        /// Rsa class does not have any public contructors.
        /// Use Rsa.FromPrivateKey() or Rsa.FromPublicKey() instead.
        /// </summary>
        private Rsa()
        {

        }


        private RSACryptoServiceProvider _rsa;
        private bool _initiatedWithPublicKey = false;


        /// <summary>
        /// Creates an Rsa object which can be used for performing RSA encryption, decryption and signing operations using the passed in key.
        /// </summary>
        /// <param name="pemPrivateKey">RSA private key in OpenSSL PEM format.</param>
        /// <returns>Rsa object</returns>
        public static Rsa FromPrivateKey(string pemPrivateKey)
        {
            try
            {
                Rsa rsa = new Rsa();
                byte[] privateKey = DecodeOpenSSLPrivateKey(pemPrivateKey);
                rsa._rsa = DecodeRSAPrivateKey(privateKey);
                rsa._initiatedWithPublicKey = false;
                /*string bString = "";
                foreach (byte b in privateKey)
                {
                    bString += b + " ";
                }*/
                Log.Debug("Private key loaded successfully: " + (rsa._rsa != null));
                //Log.Debug(bString);
                return rsa;
            }
            catch (Exception ex)
            {
                Log.Error("Exception loading private key: " + ex.Message + "\n" + ex.StackTrace);
                throw;
            }
        }

        /// <summary>
        /// Creates an Rsa object which an be used for verifying a signature using the passed in public key.
        /// </summary>
        /// <param name="pemPublicKey">RSA public key in OpenSSL PEM format.</param>
        /// <returns>Rsa object (limited to public key operations)</returns>
        public static Rsa FromPublicKey(string pemPublicKey)
        {
            try
            {
                Rsa rsa = new Rsa();
                byte[] publicKey = DecodeOpenSSLPublicKey(pemPublicKey);
                rsa._rsa = DecodeX509PublicKey(publicKey);
                rsa._initiatedWithPublicKey = true;
                Log.Debug("Public key loaded successfully: " + (rsa._rsa != null));
                return rsa;
            }
            catch (Exception ex)
            {
                Log.Error("Exception loading public key: " + ex.Message + "\n" + ex.StackTrace);
                throw;
            }
        }


        /// <summary>
        /// Encrypts a message using a public key.
        /// Padding: PKCS#1 
        /// Data encoding: UTF-8
        /// </summary>
        /// <param name="message">string to be encrypted</param>
        /// <returns>Encrypted message (as base64 encoded string)</returns>
        public string PublicEncrypt(string message)
        {            
            byte[] encodedData = Encoding.UTF8.GetBytes(message);
            byte[] encryptedData = _rsa.Encrypt(encodedData, false);
            return Convert.ToBase64String(encryptedData);
        }

        /// <summary>
        /// Decrypts a base64 encoded string which was encrypted using a public key.
        /// Padding: PKCS#1 
        /// Data encoding: UTF-8
        /// </summary>
        /// <param name="encryptedMessage">Encrypted message</param>
        /// <returns>Decrypted message</returns>
        public string PrivateDecrypt(string encryptedMessage)
        {
            if (_initiatedWithPublicKey)
                throw new Exception("Rsa must be initiated with a private key to enable private key decryption");
            byte[] encryptedData = Convert.FromBase64String(encryptedMessage);
            byte[] decryptedData = _rsa.Decrypt(encryptedData, false);
            return Encoding.UTF8.GetString(decryptedData);
        }



        /// <summary>
        /// Creates a signature by hashing the message using the SHA1 algorithm and encrypting the hash with a private key.
        /// Message is converted to an array of UTF-8 encoded bytes before being hashed.
        /// </summary>
        /// <param name="message">Message to be signed</param>
        /// <returns></returns>
        public string SHA1Sign(string message)
        {
            if (_initiatedWithPublicKey)
                throw new Exception("Rsa must be initiated with a private key to sign a message.");
            byte[] data = Encoding.UTF8.GetBytes(message);
            byte[] signature = _rsa.SignData(data, new SHA1CryptoServiceProvider());
            string encodedSignature = Convert.ToBase64String(signature);
            return encodedSignature;
        }

        /// <summary>
        /// Verifies that a signature was produced from the given message.
        /// </summary>
        /// <param name="message"></param>
        /// <param name="signature"></param>
        /// <returns>True if signature matches message</returns>
        public bool SHA1Verify(string message, string signature)
        {
            if (message == null || signature == null)
                return false;
            byte[] messageBytes = Encoding.UTF8.GetBytes(message);
            byte[] signatureBytes = Convert.FromBase64String(signature);
            return _rsa.VerifyData(messageBytes, new SHA1CryptoServiceProvider(), signatureBytes);
        }



        #region PEM key decoding

        //Source: http://www.jensign.com/opensslkey/index.html
        //Adapted from: http://www.jensign.com/opensslkey/opensslkey.cs (included in this project for reference purposes)
        //See copywrite notice in opensslkey.cs

        const String pemprivheader = "-----BEGIN RSA PRIVATE KEY-----";
        const String pemprivfooter = "-----END RSA PRIVATE KEY-----";
        const String pempubheader = "-----BEGIN PUBLIC KEY-----";
        const String pempubfooter = "-----END PUBLIC KEY-----";

        //--------   Get the binary RSA PUBLIC key   --------
        protected static byte[] DecodeOpenSSLPublicKey(String instr)
        {
            String pemstr = instr.Trim();
            byte[] binkey;
            if (!pemstr.StartsWith(pempubheader) || !pemstr.EndsWith(pempubfooter))
                return null;
            StringBuilder sb = new StringBuilder(pemstr);
            sb.Replace(pempubheader, "");  //remove headers/footers, if present
            sb.Replace(pempubfooter, "");
            String pubstr = sb.ToString().Trim();	//get string after removing leading/trailing whitespace
            return Convert.FromBase64String(pubstr);
        }

        //------- Parses binary asn.1 X509 SubjectPublicKeyInfo; returns RSACryptoServiceProvider ---
        protected static RSACryptoServiceProvider DecodeX509PublicKey(byte[] x509key)
        {
            // encoded OID sequence for  PKCS #1 rsaEncryption szOID_RSA_RSA = "1.2.840.113549.1.1.1"
            byte[] SeqOID = { 0x30, 0x0D, 0x06, 0x09, 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x01, 0x05, 0x00 };
            byte[] seq = new byte[15];
            // ---------  Set up stream to read the asn.1 encoded SubjectPublicKeyInfo blob  ------
            MemoryStream mem = new MemoryStream(x509key);
            BinaryReader binr = new BinaryReader(mem);    //wrap Memory Stream with BinaryReader for easy reading
            byte bt = 0;
            ushort twobytes = 0;

            try
            {

                twobytes = binr.ReadUInt16();
                if (twobytes == 0x8130)	//data read as little endian order (actual data order for Sequence is 30 81)
                    binr.ReadByte();	//advance 1 byte
                else if (twobytes == 0x8230)
                    binr.ReadInt16();	//advance 2 bytes
                else
                    return null;

                seq = binr.ReadBytes(15);		//read the Sequence OID
                if (!CompareBytearrays(seq, SeqOID))	//make sure Sequence for OID is correct
                    return null;

                twobytes = binr.ReadUInt16();
                if (twobytes == 0x8103)	//data read as little endian order (actual data order for Bit String is 03 81)
                    binr.ReadByte();	//advance 1 byte
                else if (twobytes == 0x8203)
                    binr.ReadInt16();	//advance 2 bytes
                else
                    return null;

                bt = binr.ReadByte();
                if (bt != 0x00)		//expect null byte next
                    return null;

                twobytes = binr.ReadUInt16();
                if (twobytes == 0x8130)	//data read as little endian order (actual data order for Sequence is 30 81)
                    binr.ReadByte();	//advance 1 byte
                else if (twobytes == 0x8230)
                    binr.ReadInt16();	//advance 2 bytes
                else
                    return null;

                twobytes = binr.ReadUInt16();
                byte lowbyte = 0x00;
                byte highbyte = 0x00;

                if (twobytes == 0x8102)	//data read as little endian order (actual data order for Integer is 02 81)
                    lowbyte = binr.ReadByte();	// read next bytes which is bytes in modulus
                else if (twobytes == 0x8202)
                {
                    highbyte = binr.ReadByte();	//advance 2 bytes
                    lowbyte = binr.ReadByte();
                }
                else
                    return null;
                byte[] modint = { lowbyte, highbyte, 0x00, 0x00 };   //reverse byte order since asn.1 key uses big endian order
                int modsize = BitConverter.ToInt32(modint, 0);

                byte firstbyte = binr.ReadByte();
                binr.BaseStream.Seek(-1, SeekOrigin.Current);

                if (firstbyte == 0x00)
                {	//if first byte (highest order) of modulus is zero, don't include it
                    binr.ReadByte();	//skip this null byte
                    modsize -= 1;	//reduce modulus buffer size by 1
                }

                byte[] modulus = binr.ReadBytes(modsize);	//read the modulus bytes

                if (binr.ReadByte() != 0x02)			//expect an Integer for the exponent data
                    return null;
                int expbytes = (int)binr.ReadByte();		// should only need one byte for actual exponent data (for all useful values)
                byte[] exponent = binr.ReadBytes(expbytes);


                // ------- create RSACryptoServiceProvider instance and initialize with public key -----
                RSACryptoServiceProvider.UseMachineKeyStore = true; //Modification (RVO): avoid IIS user security restrictions
                RSACryptoServiceProvider RSA = new RSACryptoServiceProvider();
                RSAParameters RSAKeyInfo = new RSAParameters();
                RSAKeyInfo.Modulus = modulus;
                RSAKeyInfo.Exponent = exponent;
                RSA.ImportParameters(RSAKeyInfo);
                return RSA;
            }
            finally { binr.Close(); }

        }
        
        //------- Parses binary ans.1 RSA private key; returns RSACryptoServiceProvider  ---
        protected static RSACryptoServiceProvider DecodeRSAPrivateKey(byte[] privkey)
        {
            byte[] MODULUS, E, D, P, Q, DP, DQ, IQ;

            // ---------  Set up stream to decode the asn.1 encoded RSA private key  ------
            MemoryStream mem = new MemoryStream(privkey);
            BinaryReader binr = new BinaryReader(mem);    //wrap Memory Stream with BinaryReader for easy reading
            byte bt = 0;
            ushort twobytes = 0;
            int elems = 0;
            try
            {
                twobytes = binr.ReadUInt16();
                if (twobytes == 0x8130)	//data read as little endian order (actual data order for Sequence is 30 81)
                    binr.ReadByte();	//advance 1 byte
                else if (twobytes == 0x8230)
                    binr.ReadInt16();	//advance 2 bytes
                else
                    return null;

                twobytes = binr.ReadUInt16();
                if (twobytes != 0x0102)	//version number
                    return null;
                bt = binr.ReadByte();
                if (bt != 0x00)
                    return null;


                //------  all private key components are Integer sequences ----
                elems = GetIntegerSize(binr);
                MODULUS = binr.ReadBytes(elems);

                elems = GetIntegerSize(binr);
                E = binr.ReadBytes(elems);

                elems = GetIntegerSize(binr);
                D = binr.ReadBytes(elems);

                elems = GetIntegerSize(binr);
                P = binr.ReadBytes(elems);

                elems = GetIntegerSize(binr);
                Q = binr.ReadBytes(elems);

                elems = GetIntegerSize(binr);
                DP = binr.ReadBytes(elems);

                elems = GetIntegerSize(binr);
                DQ = binr.ReadBytes(elems);

                elems = GetIntegerSize(binr);
                IQ = binr.ReadBytes(elems);


                // ------- create RSACryptoServiceProvider instance and initialize with public key -----
                RSACryptoServiceProvider.UseMachineKeyStore = true; //Modification (RVO): avoid IIS user security restrictions
                RSACryptoServiceProvider RSA = new RSACryptoServiceProvider();
                RSAParameters RSAparams = new RSAParameters();
                RSAparams.Modulus = MODULUS;
                RSAparams.Exponent = E;
                RSAparams.D = D;
                RSAparams.P = P;
                RSAparams.Q = Q;
                RSAparams.DP = DP;
                RSAparams.DQ = DQ;
                RSAparams.InverseQ = IQ;
                RSA.ImportParameters(RSAparams);
                return RSA;
            }
            finally { binr.Close(); }
        }
        
        protected static int GetIntegerSize(BinaryReader binr)
        {
            byte bt = 0;
            byte lowbyte = 0x00;
            byte highbyte = 0x00;
            int count = 0;
            bt = binr.ReadByte();
            if (bt != 0x02)		//expect integer
                return 0;
            bt = binr.ReadByte();

            if (bt == 0x81)
                count = binr.ReadByte();	// data size in next byte
            else
                if (bt == 0x82)
                {
                    highbyte = binr.ReadByte();	// data size in next 2 bytes
                    lowbyte = binr.ReadByte();
                    byte[] modint = { lowbyte, highbyte, 0x00, 0x00 };
                    count = BitConverter.ToInt32(modint, 0);
                }
                else
                {
                    count = bt;		// we already have the data size
                }



            while (binr.ReadByte() == 0x00)
            {	//remove high order zeros in data
                count -= 1;
            }
            binr.BaseStream.Seek(-1, SeekOrigin.Current);		//last ReadByte wasn't a removed zero, so back up a byte
            return count;
        }
        
        //-----  Get the binary RSA PRIVATE key, decrypting if necessary ----
        protected static byte[] DecodeOpenSSLPrivateKey(String instr)
        {
            String pemstr = instr.Trim();
            byte[] binkey;
            if (!pemstr.StartsWith(pemprivheader) || !pemstr.EndsWith(pemprivfooter))
                return null;

            StringBuilder sb = new StringBuilder(pemstr);
            sb.Replace(pemprivheader, "");  //remove headers/footers, if present
            sb.Replace(pemprivfooter, "");

            return Convert.FromBase64String(sb.ToString().Trim());
        }
        
        protected static bool CompareBytearrays(byte[] a, byte[] b)
        {
            if (a.Length != b.Length)
                return false;
            int i = 0;
            foreach (byte c in a)
            {
                if (c != b[i])
                    return false;
                i++;
            }
            return true;
        }

        #endregion

    }
}
